package model;

import main.Simulator;
import main.Util;
import enums.TxProcedure;
import event.ACKTimeoutEvent;
import event.Event;
import event.PacketArrivedAtRouterEvent;

/**
 * Classe que representa a entidade transmissora do protocolo TCP.
 */
public abstract class TxTCP {

	Simulator simulator;
	private int sessionId;
	private double propagationTime;
	private double transmissionTime;
	
	int MSS;
	double threshold;
	double cwnd;
	long cwndStart;
	long cwndEnd;
	long cwndEndFR;
	long lastMissedByte;
	long duplicatedACKCount;
	long byteTimed;
	TxProcedure procedure;
	
	double A;
	double D;
	double RTO;
	
	public double getCwnd() {
		return cwnd;
	}
	
	public TxTCP(Simulator simulator, int sessionId, double propagationTime) {
		this.simulator = simulator;
		this.sessionId = sessionId;
		this.propagationTime = propagationTime;
		this.transmissionTime = Util.convertSecondsToMillis((8.0 * simulator.getProperties().getMSS() / simulator.getProperties().getCs()));
		
		// Inicia as variáveis.
		MSS = simulator.getProperties().getMSS();
		threshold = 65535;
		cwnd = MSS;
		cwndStart = 0;
		cwndEnd = MSS;
		duplicatedACKCount = 0;
		byteTimed = 0;
		procedure = TxProcedure.SLOW_START;
		
		// Inicia o RTO com valores iniciais
		A = 4*propagationTime;
		D = 0;
		RTO = A + 4*D;
	}
	
	/**
	 * Método que atualiza o RTO.
	 * @param M RTT instantâneo
	 */
	public void updateRTO(double M) {
		double Y = M - A;
		D = D + 0.25*(Math.abs(Y) - D);
		A = A + 0.125*Y;
		RTO = A + 4*D;
	}
	
	/**
	 * Método que envia um pacote no tempo determinado.
	 * @param pack pacote a ser enviado
	 * @param startTime tempo a ser enviado
	 */
	public void sendPacket(Packet pack, double startTime) {
		// Programa o envio do pacote no tempo determinado.
		pack.setSentTime(startTime);
		double arrivedTime = startTime + propagationTime + transmissionTime;
		Event packetArrivedAtRouterEvent = new PacketArrivedAtRouterEvent(arrivedTime, pack);
		simulator.addEvent(packetArrivedAtRouterEvent);
		
		// Programa o evento de timeout do pacote.
		double timeout = startTime + RTO;
		Event ackTimeoutEvent = new ACKTimeoutEvent(timeout, pack);
		simulator.addEvent(ackTimeoutEvent);
	}
	
	/**
	 * Método que trata o recebimento de um ACK.
	 * @param ack ACK recebido pelo transmissor
	 */
	public void receiveACK(ACK ack) {
		// Remove o timeout do pacote.
		simulator.removeEvent(new ACKTimeoutEvent(0, ack.getPack()));

		// Próximo byte requisitado.
		long nextByte = ack.getMissingBytes().getFirst();
		if (nextByte < cwndStart) {
			return;
		}
		
		// Verifica qual estado do transmissor para o incremento da janela.
		switch (procedure) {
		case SLOW_START:
			cwnd += MSS;
			if (cwnd >= threshold) {
				procedure = TxProcedure.CONGESTION_AVOIDANCE;
			}
			break;
		case CONGESTION_AVOIDANCE:
			cwnd += MSS * MSS / cwnd;
			break;
		case FAST_RECOVERY:
			cwnd += MSS;
			break;
		default:
			throw new IllegalStateException("Estado do transmissor inv�lido.");
		}
		
		if (procedure.equals(TxProcedure.FAST_RECOVERY)) { // FAST RECOVERY
			if (nextByte < cwndEndFR) {
				cwnd -= nextByte - cwndStart;
				if (cwnd < MSS) {
					cwnd = MSS;
				}

				// Retransmite os bytes com SACK.
				double startTime = simulator.getTime();
				for (long missedByte : ack.getMissingBytes()) {
					if (missedByte > lastMissedByte) {
						Packet pack = new Packet(sessionId, missedByte);
						simulator.removeEvent(new ACKTimeoutEvent(0, pack));
						sendPacket(pack, startTime);
						startTime += transmissionTime;
						lastMissedByte = missedByte;
					}
				}
			} else {
				cwnd = threshold;
				procedure = TxProcedure.CONGESTION_AVOIDANCE;
			}
			cwndStart = nextByte;
		} else { // SLOW START e CONGESTION AVOIDANCE
			if (nextByte == cwndStart) {
				duplicatedACKCount++;
				if (duplicatedACKCount == 3) {
					duplicatedACKCount = 0;
					cwndEndFR = cwndEnd;
					simulator.removeTimeoutEvents(sessionId);
					byteTimed = nextByte;
					
					threshold = cwnd/2;
					cwnd = threshold + 3*MSS;
					procedure = TxProcedure.FAST_RECOVERY;
					
					// Retransmite os bytes com SACK.
					double startTime = simulator.getTime();
					for (long missedByte : ack.getMissingBytes()) {
						Packet pack = new Packet(sessionId, missedByte);
						simulator.removeEvent(new ACKTimeoutEvent(0, pack));
						sendPacket(pack, startTime);
						startTime += transmissionTime;
						lastMissedByte = missedByte;
					}
				}
			} else {
				duplicatedACKCount = 0;
				cwndStart = nextByte;

				// medição do RTT instantâneo
				if (ack.getPack().getFirstByte() == byteTimed) {
					updateRTO(simulator.getTime() - ack.getPack().getSentTime());
					byteTimed = nextByte;
				}
			}
		}
		
		// Verifica o final da janela
		if (cwndEnd < cwndStart) {
			cwndEnd = cwndStart;
		}
		
		// Envia os próximos pacotes
		double startTime = simulator.getTime();
		while (cwndEnd < cwndStart+cwnd) {
			Packet pack = new Packet(sessionId, cwndEnd);
			sendPacket(pack, startTime);
			startTime += transmissionTime;
			cwndEnd += MSS;
		}
	}
	
	/**
	 * Método que trata o evento de timeout.
	 * @param pack pacote que recebeu timeout
	 */
	public void handleTimeout(Packet pack) {
		duplicatedACKCount = 0;
		simulator.removeTimeoutEvents(sessionId);
		byteTimed = pack.getFirstByte();
		
		threshold = cwnd/2;
		cwnd = MSS;
		cwndEnd = cwndStart + MSS;
		procedure = TxProcedure.SLOW_START;
		
		sendPacket(pack, simulator.getTime());
	}
	
}
